/**
 * @author Vitor Franco do Carmo (vitor.carmo@capgemini.com)
 * @since 12/06/2013
 *
 */
package br.com.vfcfw.commons.util;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;
import java.net.URL;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.PropertyException;
import javax.xml.bind.Unmarshaller;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;

import org.apache.log4j.Logger;

import br.com.vfcfw.commons.exception.ApplicationException;

/**
 * Classe utilitária para abstrair a utilização do JAXB.
 * 
 * @author Vitor Franco do Carmo (vitor.carmo@capgemini.com)
 * @since 12/06/2013
 * 
 */
public class JAXBUtil {

    private static final Logger LOGGER = Logger.getLogger(JAXBUtil.class);

    public static final String W3C_XML_SCHEMA_INSTANCE_NS_URI = "http://www.w3.org/2001/XMLSchema-instance";

    /**
     * Construtor da classe.
     */
    private JAXBUtil() {

        super();
    }

    /**
     * Cria o arquivo XML, usando o encoding UTF-8, baseado no objeto anotado com JAXB.
     * 
     * @param classe Classe do objeto principal (<tt>anotado com <b>@XmlRootElement</b></tt>).
     * @param objeto Objeto principal.
     * @param caminhoNomeArquivo Caminho completo e nome do arquivo XML que será gerado.
     */
    public static <T> void criarArquivoXML(Class<T> classe, Object objeto, String caminhoNomeArquivo) {

        criarArquivoXML(classe, objeto, null, caminhoNomeArquivo);
    }

    /**
     * Cria o arquivo XML baseado no objeto anotado com JAXB.
     * 
     * @param classe Classe do objeto principal (<tt>anotado com <b>@XmlRootElement</b></tt>).
     * @param objeto Objeto principal.
     * @param encoding Encoding usado na geração do XML (<tt>UTF-8 é o padrão</tt>).
     * @param caminhoNomeArquivo Caminho completo e nome do arquivo XML que será gerado.
     */
    public static <T> void criarArquivoXML(Class<T> classe, Object objeto, String encoding, String caminhoNomeArquivo) {

        LogUtil.debug(LOGGER, "Criando arquivo XML baseado no objeto anotado.");

        try {

            Marshaller marshaller = criarMarshaller(classe, encoding);
            marshaller.marshal(objeto, new FileOutputStream(caminhoNomeArquivo));

        } catch (Exception e) {
            LogUtil.error(LOGGER, e, "Erro ao criar arquivo XML baseado no objeto anotado.");
            throw new ApplicationException(e);
        }
    }

    /**
     * Cria o arquivo XML, usando o encoding UTF-8, baseado no objeto anotado com JAXB.
     * 
     * @param classe Classe do objeto principal (<tt>anotado com <b>@XmlRootElement</b></tt>).
     * @param objeto Objeto principal.
     * @param caminhoNomeArquivo Caminho completo e nome do arquivo XML que será gerado.
     * @return {@link String} Arquivo XML.
     */
    public static <T> String criarArquivoXMLString(Class<T> classe, Object objeto) {

        return criarArquivoXMLString(classe, objeto, null);
    }

    /**
     * Cria o arquivo XML baseado no objeto anotado com JAXB.
     * 
     * @param classe Classe do objeto principal (<tt>anotado com <b>@XmlRootElement</b></tt>).
     * @param objeto Objeto principal.
     * @param encoding Encoding usado na geração do XML (<tt>UTF-8 é o padrão</tt>).
     * @param caminhoNomeArquivo Caminho completo e nome do arquivo XML que será gerado.
     * @return {@link String} Arquivo XML.
     */
    public static <T> String criarArquivoXMLString(Class<T> classe, Object objeto, String encoding) {

        String arquivoXML = null;

        LogUtil.debug(LOGGER, "Criando a String do arquivo XML baseado no objeto anotado.");

        try {

            Writer writer = new StringWriter();

            Marshaller marshaller = criarMarshaller(classe, encoding);
            marshaller.marshal(objeto, writer);

            arquivoXML = writer.toString();

            LogUtil.debug(LOGGER, "Arquivo XML:\n%s", arquivoXML);

        } catch (Exception e) {
            LogUtil.error(LOGGER, e, "Erro ao criar a String do arquivo XML baseado no objeto anotado.");
            throw new ApplicationException(e);
        }
        return arquivoXML;
    }

    /**
     * Retorna o objeto anotado com JAXB baseado no arquivo XML.
     * 
     * @param classe Classe do objeto principal (<tt>anotado com <b>@XmlRootElement</b></tt>).
     * @param caminhoNomeArquivo Caminho completo e nome do arquivo XML.
     * @return Objeto.
     */
    @SuppressWarnings("unchecked")
    public static <T> T lerArquivoXML(Class<T> classe, String caminhoNomeArquivo) {

        T objetoRetorno = null;

        LogUtil.debug(LOGGER, "Lendo o arquivo XML e transformando no objeto anotado.");

        try {

            Unmarshaller unmarshaller = criarUnmarshaller(classe);

            objetoRetorno = (T) unmarshaller.unmarshal(new FileInputStream(caminhoNomeArquivo));

        } catch (Exception e) {
            LogUtil.error(LOGGER, e, "Erro ao ler o arquivo XML e transformar no objeto anotado.");
            throw new ApplicationException(e);
        }
        return objetoRetorno;
    }

    /**
     * Retorna o objeto anotado com JAXB baseado no arquivo XML.
     * 
     * @param classe Classe do objeto principal (<tt>anotado com <b>@XmlRootElement</b></tt>).
     * @param arquivoXML Arquivo XML no formato {@link String}.
     * @return Objeto.
     */
    @SuppressWarnings("unchecked")
    public static <T> T lerArquivoXMLString(Class<T> classe, String arquivoXML) {

        T objetoRetorno = null;

        LogUtil.debug(LOGGER, "Lendo a String do XML e transformando no objeto anotado.");

        try {

            Unmarshaller unmarshaller = criarUnmarshaller(classe);

            objetoRetorno = (T) unmarshaller.unmarshal(new StringReader(arquivoXML));

        } catch (Exception e) {
            LogUtil.error(LOGGER, e, "Erro ao ler a String do XML e transformar no objeto anotado.");
            throw new ApplicationException(e);
        }
        return objetoRetorno;
    }

    /**
     * Retorna o objeto anotado com JAXB baseado no Schema XML.
     * 
     * @param classe {@link Class} Classe anotada com JAXB que terá uma instância retornada.
     * @param arquivoXML Arquivo XML no formato {@link String}.
     * @param schemaXML Schema XML.
     * @return
     */
    @SuppressWarnings("unchecked")
    public static <T> T lerArquivoXMLString(Class<T> classe, String arquivoXML, URL schemaXML) {

        T objetoRetorno = null;

        LogUtil.debug(LOGGER, "Lendo a String do XML e transformando no objeto anotado.");

        try {

            Unmarshaller unmarshaller = criarUnmarshaller(classe);

            if (schemaXML != null) {

                SchemaFactory schemaFactory = SchemaFactory.newInstance(W3C_XML_SCHEMA_INSTANCE_NS_URI);
                Schema schema = schemaFactory.newSchema(schemaXML);
                unmarshaller.setSchema(schema);
            }

            objetoRetorno = (T) unmarshaller.unmarshal(new StringReader(arquivoXML));

        } catch (Exception e) {
            LogUtil.error(LOGGER, e, "Erro ao ler a String do XML e transformar no objeto anotado.");
            throw new ApplicationException(e);
        }
        return objetoRetorno;
    }

    /**
     * Cria o {@link Marshaller} baseado no tipo da classe informada.
     * 
     * @param classe {@link Class} Tipo da classe.
     * @param encoding Encoding usado na geração do XML (<tt>UTF-8 é o padrão</tt>).
     * @return {@link Marshaller}
     * @throws JAXBException
     * @throws PropertyException
     */
    private static <T> Marshaller criarMarshaller(Class<T> classe, String encoding) throws JAXBException,
        PropertyException {

        LogUtil.debug(LOGGER, "Criando o Marshaller da classe: %s.", classe.getSimpleName());

        encoding = StringUtil.possuiValor(encoding) ? encoding : "UTF-8";

        JAXBContext context = JAXBContext.newInstance(classe);
        Marshaller marshaller = context.createMarshaller();
        marshaller.setProperty(Marshaller.JAXB_ENCODING, encoding);
        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
        return marshaller;
    }

    /**
     * Cria o {@link Unmarshaller} baseado no tipo da classe informada.
     * 
     * @param classe {@link Class} Tipo da classe.
     * @return {@link Unmarshaller}.
     * @throws JAXBException
     */
    private static <T> Unmarshaller criarUnmarshaller(Class<T> classe) throws JAXBException {

        LogUtil.debug(LOGGER, "Criando o Unmarshaller da classe: %s.", classe.getSimpleName());

        JAXBContext context = JAXBContext.newInstance(classe);

        return context.createUnmarshaller();
    }

}
